name: Conformance Ginkgo (ci-ginkgo)

# Any change in triggers needs to be reflected in the concurrency group.
on:
  workflow_dispatch:
    inputs:
      PR-number:
        description: "Pull request number."
        required: true
      context-ref:
        description: "Context in which the workflow runs. If PR is from a fork, will be the PR target branch (general case). If PR is NOT from a fork, will be the PR branch itself (this allows committers to test changes to workflows directly from PRs)."
        required: true
      SHA:
        description: "SHA under test (head of the PR branch)."
        required: true
      extra-args:
        description: "[JSON object] Arbitrary arguments passed from the trigger comment via regex capture group. Parse with 'fromJson(inputs.extra-args).argName' in workflow."
        required: false
        default: '{}'
  # Run every 6 hours
  schedule:
    - cron:  '0 1/6 * * *'

# By specifying the access of one of the scopes, all of those that are not
# specified are set to 'none'.
permissions:
  # To be able to access the repository with actions/checkout
  contents: read
  # To allow retrieving information from the PR API
  pull-requests: read
  # To be able to set commit status
  statuses: write

concurrency:
  # Structure:
  # - Workflow name
  # - Event type
  # - A unique identifier depending on event type:
  #   - schedule: SHA
  #   - workflow_dispatch: PR number
  #
  # This structure ensures a unique concurrency group name is generated for each
  # type of testing, such that re-runs will cancel the previous run.
  group: |
    ${{ github.workflow }}
    ${{ github.event_name }}
    ${{
      (github.event_name == 'schedule' && github.sha) ||
      (github.event_name == 'workflow_dispatch' && github.event.inputs.PR-number)
    }}
  cancel-in-progress: true

env:
  check_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}

jobs:
  setup-vars:
    name: Setup Vars
    runs-on: ubuntu-latest
    outputs:
      SHA: ${{ steps.vars.outputs.SHA }}
      context-ref: ${{ steps.vars.outputs.context-ref }}
      owner: ${{ steps.vars.outputs.owner }}
    steps:
      - name: Set up job variables
        id: vars
        run: |
          if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
            SHA="${{ inputs.SHA }}"
            CONTEXT_REF="${{ inputs.context-ref }}"
            OWNER="${{ inputs.PR-number }}"
          else
            SHA="${{ github.sha }}"
            CONTEXT_REF="${{ github.sha }}"
            OWNER="${{ github.ref_name }}"
            OWNER="${OWNER/./-}"
          fi

          echo SHA=${SHA} >> $GITHUB_OUTPUT
          echo context-ref=${CONTEXT_REF} >> $GITHUB_OUTPUT
          echo owner=${OWNER} >> $GITHUB_OUTPUT

  commit-status-start:
    name: Commit Status Start
    runs-on: ubuntu-latest
    steps:
      - name: Set initial commit status
        uses: myrotvorets/set-commit-status-action@38f3f27c7d52fb381273e95542f07f0fba301307 # v2.0.0
        with:
          sha: ${{ inputs.SHA || github.sha }}

  # Pre-build the ginkgo binary so that we don't have to build it for all
  # runners.
  build-ginkgo-binary:
    runs-on: ubuntu-latest
    name: Build Ginkgo E2E
    timeout-minutes: 30
    steps:
      # If any of these steps are modified, please update the copy of these
      # steps further down under the 'setup-and-test' jobs.

      # Warning: since this is a privileged workflow, subsequent workflow job
      # steps must take care not to execute untrusted code.
      - name: Checkout pull request branch (NOT TRUSTED)
        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
        with:
          ref: ${{ inputs.SHA || github.sha }}
          persist-credentials: false

      # Load Ginkgo build from GitHub
      - name: Load ginkgo E2E from GH cache
        uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4.0.0
        id: cache
        with:
          path: /tmp/.ginkgo-build/
          key: ${{ runner.os }}-ginkgo-e2e-${{ hashFiles('**/*.go') }}

      - name: Install Go
        if: ${{ steps.cache.outputs.cache-hit != 'true' }}
        uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0
        with:
          # renovate: datasource=golang-version depName=go
          go-version: 1.22.0

      - name: Build Ginkgo
        if: ${{ steps.cache.outputs.cache-hit != 'true' }}
        shell: bash
        run: |
          go install github.com/onsi/ginkgo/ginkgo@v1.16.5
          mkdir -p /tmp/.ginkgo-build

      - name: Build Test
        if: ${{ steps.cache.outputs.cache-hit != 'true' }}
        shell: bash
        run: |
          cd test
          /home/runner/go/bin/ginkgo build
          strip test.test
          tar -cz test.test -f test.tgz

      - name: Store Ginkgo Test in GitHub cache path
        if: ${{ steps.cache.outputs.cache-hit != 'true' }}
        shell: bash
        run: |
          mkdir -p /tmp/.ginkgo-build/
          if [ -f test/test.tgz ]; then
            cp test/test.tgz /tmp/.ginkgo-build/
            echo "file copied"
          fi

  wait-for-images:
    needs: setup-vars
    runs-on: ubuntu-latest
    name: Wait for images
    timeout-minutes: 30
    steps:
      - name: Checkout context ref (trusted)
        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
        with:
          ref: ${{ inputs.context-ref || github.sha }}
          persist-credentials: false

      - name: Set Environment Variables
        uses: ./.github/actions/set-env-variables

      - name: Wait for images to be available
        timeout-minutes: 30
        shell: bash
        run: |
          for image in cilium-ci operator-generic-ci hubble-relay-ci ; do
            until docker manifest inspect quay.io/${{ env.QUAY_ORGANIZATION_DEV }}/$image:${{ needs.setup-vars.outputs.SHA }} &> /dev/null; do sleep 45s; done
          done

  generate-matrix:
    name: Generate Job Matrix from YAMLs
    needs: setup-vars
    runs-on: ubuntu-latest
    outputs:
      matrix: ${{ steps.set-matrix.outputs.matrix }}
    steps:
      - name: Checkout context ref (trusted)
        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
        with:
          ref: ${{ inputs.context-ref || github.sha }}
          persist-credentials: false

      - name: Convert YAML to JSON
        run: |
          work_dir=".github/actions/ginkgo"
          destination_directory="/tmp/generated/ginkgo"
          mkdir -p "${destination_directory}"
          for file in "${work_dir}"/main*.yaml; do
              if [[ -f "$file" ]]; then
                  filename=$(basename "$file")
                  new_filename="${filename%.yaml}.json"

                  yq -o=json "${file}" | jq . > "${destination_directory}/${new_filename}"
              fi
          done

      - name: Generate Matrix
        id: set-matrix
        run: |
          if ${{ github.event_name == 'schedule' }}; then
            k8s_versions_to_run='main-scheduled.json'
          else
            k8s_versions_to_run='main-prs.json'
          fi

          # Generate a Matrix from all k8s versions defined in '${k8s_versions_to_run}'
          # combined with 'main-focus.yaml'.
          # Use 'main-k8s-versions.yaml' to
          # retrieve which kernel versions should be used for which k8s version.

          dir="/tmp/generated/ginkgo"
          cd ${dir}
          jq --argjson prs "$(jq '.["k8s-version"]' ${k8s_versions_to_run})" \
            --argfile focus main-focus.json \
            '.include |= map(select(.["k8s-version"] as $k | $prs[] | select($k == .))) + $focus.include |
            . + {"k8s-version": $prs} |
            .focus = $focus.focus | .exclude = $focus.exclude' \
            main-k8s-versions.json> /tmp/merged.json
          echo "Generated matrix:"
          cat /tmp/merged.json
          echo "matrix=$(jq -c . < /tmp/merged.json)" >> $GITHUB_OUTPUT

  setup-and-test:
    needs: [setup-vars, build-ginkgo-binary, generate-matrix, wait-for-images]
    runs-on:
      group: ginkgo-runners
    timeout-minutes: 35
    name: "E2E Test (${{ matrix.k8s-version }}, ${{matrix.focus}})"
    env:
      job_name: "E2E Test (${{ matrix.k8s-version }}, ${{matrix.focus}})"
    strategy:
      fail-fast: false
      max-parallel: 60
      matrix: ${{ fromJSON(needs.generate-matrix.outputs.matrix) }}

    steps:
      - name: Checkout context ref (trusted)
        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
        with:
          ref: ${{ inputs.context-ref || github.sha }}
          persist-credentials: false

      - name: Set Environment Variables
        uses: ./.github/actions/set-env-variables

      # Warning: since this is a privileged workflow, subsequent workflow job
      # steps must take care not to execute untrusted code.
      - name: Checkout pull request branch (NOT TRUSTED)
        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
        with:
          ref: ${{ inputs.SHA || github.sha }}
          persist-credentials: false

      - name: Install cilium-cli
        shell: bash
        run: |
          cid=$(docker create quay.io/cilium/cilium-cli-ci:latest ls)
          docker cp $cid:/usr/local/bin/cilium ./cilium-cli
          docker rm $cid

      - name: Install helm
        shell: bash
        run: |
          # renovate: datasource=github-releases depName=helm/helm
          HELM_VERSION=v3.13.1
          wget "https://get.helm.sh/helm-${HELM_VERSION}-linux-amd64.tar.gz"
          tar -xf "helm-${HELM_VERSION}-linux-amd64.tar.gz"
          mv ./linux-amd64/helm ./helm

      - name: Provision LVH VMs
        id: provision-vh-vms
        uses: cilium/little-vm-helper@9d758b756305e83718a51b792a5aeabd022a39ec # v0.0.16
        with:
          test-name: datapath-conformance
          install-dependencies: true
          image-version: ${{ matrix.kernel }}
          host-mount: ./
          cpu: 4
          mem: 12G
          cmd: |
            git config --global --add safe.directory /host
            mv /host/helm /usr/bin
            mv /host/cilium-cli /usr/bin

      - name: Provision kind
        timeout-minutes: 5
        uses: cilium/little-vm-helper@9d758b756305e83718a51b792a5aeabd022a39ec # v0.0.16
        with:
          provision: 'false'
          cmd: |
            cd /host/
            if [[ "${{ matrix.kernel }}" == bpf-next-* ]]; then
              ./contrib/scripts/kind.sh "" 2 "" "${{ matrix.kube-image }}" "none" "${{ matrix.ip-family }}"
              kubectl label node kind-worker2 cilium.io/ci-node=kind-worker2
              # Avoid re-labeling this node by setting "node-role.kubernetes.io/controlplane"
              kubectl label node kind-worker2 node-role.kubernetes.io/controlplane=
            else
              ./contrib/scripts/kind.sh "" 1 "" "${{ matrix.kube-image }}" "iptables" "${{ matrix.ip-family }}"
            fi
            # Some tests using demo-customcalls.yaml are mounting this directoy
            mkdir -p /home/vagrant/go/src/github.com/cilium
            ln -s /host /home/vagrant/go/src/github.com/cilium/cilium
            git config --add safe.directory /cilium

      # Load Ginkgo build from GitHub
      - name: Load ${{ matrix.name }} Ginkgo build from GitHub
        uses: actions/cache/restore@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4.0.0
        id: cache
        with:
          path: /tmp/.ginkgo-build/
          key: ${{ runner.os }}-ginkgo-e2e-${{ hashFiles('**/*.go') }}

      # Re-build the tests if it was a cache miss.
      - name: Install Go
        if: ${{ steps.cache.outputs.cache-hit != 'true' }}
        uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0
        with:
          # renovate: datasource=golang-version depName=go
          go-version: 1.22.0

      - name: Build Ginkgo
        if: ${{ steps.cache.outputs.cache-hit != 'true' }}
        shell: bash
        run: |
          go install github.com/onsi/ginkgo/ginkgo@v1.16.5
          mkdir -p /tmp/.ginkgo-build

      - name: Build Test
        if: ${{ steps.cache.outputs.cache-hit != 'true' }}
        shell: bash
        run: |
          cd test
          /home/runner/go/bin/ginkgo build
          strip test.test
          tar -cz test.test -f test.tgz

      - name: Store Ginkgo Test in GitHub cache path
        if: ${{ steps.cache.outputs.cache-hit != 'true' }}
        shell: bash
        run: |
          mkdir -p /tmp/.ginkgo-build/
          if [ -f test/test.tgz ]; then
            cp test/test.tgz /tmp/.ginkgo-build/
            echo "file copied"
          fi

      - name: Copy Ginkgo binary
        shell: bash
        run: |
          cd test/
          tar -xf /tmp/.ginkgo-build/test.tgz

      - name: Run tests
        id: run-tests
        timeout-minutes: 40
        uses: cilium/little-vm-helper@9d758b756305e83718a51b792a5aeabd022a39ec # v0.0.16
        with:
          provision: 'false'
          cmd: |
            cd /host/test/
            kubectl get ns -A -o wide
            kubectl get pods -A -o wide
            export K8S_NODES=2
            export NETNEXT=0
            if [[ "${{ matrix.kernel }}" == bpf-next-* ]]; then
               export KERNEL=net-next
               export NETNEXT=1
               export KUBEPROXY=0
               export K8S_NODES=3
               export NO_CILIUM_ON_NODES=kind-worker2
            elif [[ "${{ matrix.kernel }}" == 5.4-* ]]; then
               export KERNEL=54
            fi
            export K8S_VERSION=${{ matrix.k8s-version }}
            export CNI_INTEGRATION=kind
            export INTEGRATION_TESTS=true
            # GitHub actions do not support IPv6 connectivity to outside
            # world.
            export CILIUM_NO_IPV6_OUTSIDE=true
            echo "/root/go/bin/ginkgo \
             --focus=\"${{ matrix.cliFocus }}\" \
             --skip=\"${{ matrix.cliSkip }}\" \
             --seed=1679952881 \
             -v -- \
             -cilium.provision=false \
             -cilium.image=quay.io/${{ env.QUAY_ORGANIZATION_DEV }}/cilium-ci \
             -cilium.tag=${{ needs.setup-vars.outputs.SHA }}  \
             -cilium.operator-image=quay.io/${{ env.QUAY_ORGANIZATION_DEV }}/operator \
             -cilium.operator-tag=${{ needs.setup-vars.outputs.SHA }} \
             -cilium.hubble-relay-image=quay.io/${{ env.QUAY_ORGANIZATION_DEV }}/hubble-relay-ci \
             -cilium.hubble-relay-tag=${{ needs.setup-vars.outputs.SHA }} \
             -cilium.kubeconfig=/root/.kube/config \
             -cilium.provision-k8s=false \
             -cilium.operator-suffix=-ci"

              ./test.test \
               --ginkgo.focus="${{ matrix.cliFocus }}" \
               --ginkgo.skip="${{ matrix.cliSkip }}" \
               --ginkgo.seed=1679952881 \
               --ginkgo.v -- \
               -cilium.provision=false \
               -cilium.image=quay.io/${{ env.QUAY_ORGANIZATION_DEV }}/cilium-ci \
               -cilium.tag=${{ needs.setup-vars.outputs.SHA }}  \
               -cilium.operator-image=quay.io/${{ env.QUAY_ORGANIZATION_DEV }}/operator \
               -cilium.operator-tag=${{ needs.setup-vars.outputs.SHA }} \
               -cilium.hubble-relay-image=quay.io/${{ env.QUAY_ORGANIZATION_DEV }}/hubble-relay-ci \
               -cilium.hubble-relay-tag=${{ needs.setup-vars.outputs.SHA }} \
               -cilium.kubeconfig=/root/.kube/config \
               -cilium.provision-k8s=false \
               -cilium.operator-suffix=-ci

      - name: Fetch artifacts
        if: ${{ !success() && steps.provision-vh-vms.outcome == 'success' }}
        uses: cilium/little-vm-helper@9d758b756305e83718a51b792a5aeabd022a39ec # v0.0.16
        with:
          provision: 'false'
          cmd: |
            cd /host
            kubectl get pods --all-namespaces -o wide
            tar -zcf "test_results-${{ env.job_name }}.tar.gz" /host/test/test_results

      - name: Upload artifacts
        if: ${{ !success() }}
        uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
        with:
          name: cilium-sysdumps-${{ matrix.k8s-version }}-${{matrix.focus}}
          path: |
            cilium-sysdump-*.zip
            bugtool-*.tar.gz
            test_results-*.tar.gz

      - name: Fetch JUnits
        if: ${{ always() && steps.run-tests.outcome != 'skipped' }}
        shell: bash
        run: |
          mkdir -p cilium-junits
          cd test/
          junit_filename="${{ env.job_name }}.xml"
          for filename in *.xml; do cp "${filename}" "../cilium-junits/${junit_filename}"; done;

      - name: Upload JUnits [junit]
        if: ${{ always() }}
        uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
        with:
          name: cilium-junits-${{ matrix.k8s-version }}-${{matrix.focus}}
          path: cilium-junits/*.xml

      - name: Publish Test Results As GitHub Summary
        if: ${{ always() }}
        uses: aanm/junit2md@332ebf0fddd34e91b03a832cfafaa826306558f9 # v0.0.3
        with:
          junit-directory: "cilium-junits"

  merge-upload:
    if: ${{ always() }}
    name: Merge and Upload Artifacts
    runs-on: ubuntu-latest
    needs: setup-and-test
    steps:
      - name: Merge Sysdumps
        if: ${{ needs.setup-and-test.result == 'failure' }}
        uses: actions/upload-artifact/merge@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
        with:
          name: cilium-sysdumps
          pattern: cilium-sysdumps-*
          retention-days: 5
          delete-merged: true
        continue-on-error: true
      - name: Merge JUnits
        uses: actions/upload-artifact/merge@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
        with:
          name: cilium-junits
          pattern: cilium-junits-*
          retention-days: 5
          delete-merged: true

  commit-status-final:
    if: ${{ always() }}
    name: Commit Status Final
    needs: setup-and-test
    runs-on: ubuntu-latest
    steps:
      - name: Determine final commit status
        id: commit-status
        shell: bash
        run: |
          # When one of the prerequisites of setup-and-test fails, then that
          # job gets skipped. Let's convert the status so that we correctly
          # report that as a proper failure.
          if [ "${{ needs.setup-and-test.result }}" != "skipped" ]; then
            echo "status=${{ needs.setup-and-test.result }}" >> $GITHUB_OUTPUT
          else
            echo "status=failure" >> $GITHUB_OUTPUT
          fi

      - name: Set final commit status
        uses: myrotvorets/set-commit-status-action@38f3f27c7d52fb381273e95542f07f0fba301307 # v2.0.0
        with:
          sha: ${{ inputs.SHA || github.sha }}
          status: ${{ steps.commit-status.outputs.status }}
